<?php
class reCaptcha {
    const RECAPTCHA_API_SERVER = "http://www.google.com/recaptcha/api";
    const RECAPTCHA_API_SECURE_SERVER = "https://www.google.com/recaptcha/api";
    const RECAPTCHA_VERIFY_SERVER = "www.google.com";
    private $canEncrypt = false;
    private $publicKey = '';
    private $privateKey = '';
    private $response = null;
    private $settings;
    
    /**
     * Adds the reCAPTCHA AJAX API to the Javascript sources. This must be called
     * before Symfony calls include_javascripts() in order to work. As of Symfony 1.4,
     * that means you can call it in either the action or the action template, but
     * not in a component or component template.
     */
    public static function enableAjax()
    {
        $js_url = sfConfig::get('app_sf_recaptcha_plugin_ajax_api', 'http://www.google.com/recaptcha/api/js/recaptcha_ajax.js');
        sfContext::getInstance()->getResponse()->addJavascript($js_url);
    }
    
    public static function getPublicKey()
    {
        return sfConfig::get('app_sf_recaptcha_plugin_public_key', '');
    }

    /**
     * Consructor. Tests for the encryption library (for mailhide) and loads the
     * reCAPTCHA keys from app.yml.
     */
    public function reCaptcha()
    {
        if(function_exists("mcrypt_encrypt"))
            $this->canEncrypt = true;
        $this->publicKey = sfConfig::get('app_sf_recaptcha_plugin_public_key', '');
        $this->privateKey = sfConfig::get('app_sf_recaptcha_plugin_private_key', '');
        $this->settings = new reCaptchaSettings();
    }
    
    public function getSettings() {
        return $this->settings;
    }
    
    public function getJSON() {
        return $this->settings->toJSON();
    }

    public function setLanguage($lang) {
        $lang = strtolower($lang);
        $this->settings->setLanguage($lang);
    }
    
    public function setTheme($theme) {
        $theme = strtolower($theme);
        $this->settings->setTheme($theme);
    }
    
    public function setCustomThemeWidget($id) {
        $this->settings->setCustomThemeWidget($id);
    }
    

    
    public function setTabIndex($i) {
        $i = intval($i);
        $this->settings->setTabIndex($i);
    }
    
    public function setCustomTranslations(array $trans)
    {
        $this->settings->setCustomTranslations($trans);
    }

    
    /**
     * Returns true only if there was a CAPTCHA validation attempt and it passed.
     * Returns false otherwise.
     * @return true if the CAPTCHA test passed. false if there was no attempt or
     * if the attempt failed.
     */
    public function isValid()
    {
        if(empty($this->response)) return false;

        return $this->response->is_valid;
    }
    /**
     * Get an error associated with a CAPTCHA attempt. Returns the error string,
     * or an empty string if there was no error.
     * @return An unformatted error string.
     */
    public function getError()
    {
        if(empty($this->response))
            return '';
        return $this->response->error;
    }
    
    /**
     * Render an error to HTML. If this method is called without a specific
     * error, it will use the response error message if it exists. Nothing
     * is rendered if there is no error.
     * @param $error An explicit error to display.
     * @return HTML-formatted error string, or an empty string if there is no
     * error.
     */
    public function renderError($error=null)
    {
        if($error == null)
        {
            $error = ($this->response == null) ? '' : $this->response->error;
        }
        if(empty($error)) return '';
        
        return '<i>sfReCaptchaPlugin: '.$error.'</i>';
    }

    /**
     * Generates the ReCAPTCHA challenge HTML. Provides both JS and non-JS versions.
     * 
     * @param $error (optional) The error given by reCAPTCHA (default is null)
     * @param $use_ssl If set, the reCAPTCHA request will be made via HTTPS. Otherwise it will be made via HTTP (default).
     * @return HTML code that will generate the reCAPTCHA from the reCAPTCHA server.
     */

    public function getHTML($use_ssl = false)
    {
        $pubkey = $this->publicKey;
        try {
            if(empty($pubkey))
            {
                throw new reCaptchaException("Your reCAPTCHA public key is missing.");
            } else {
                $server = ($use_ssl) ? self::RECAPTCHA_API_SECURE_SERVER : self::RECAPTCHA_API_SERVER;
                        $error = $this->getError();
                $errorpart = empty($error) ? "" : "&amp;error=".$error;
                
                return $this->settings.'<script type="text/javascript" src="'. $server . '/challenge?k=' . $pubkey . $errorpart . '"></script>
<noscript>
    <iframe src="'. $server . '/noscript?k=' . $pubkey . $errorpart . '" height="300" width="500" frameborder="0"></iframe><br/>
    <textarea name="recaptcha_challenge_field" rows="3" cols="40"></textarea>
    <input type="hidden" name="recaptcha_response_field" value="manual_challenge"/>
</noscript>';
            }
        } catch(reCaptchaException $ex) {
            return $this->renderError($ex->getMessage());
        }
    }

    /**
     * Send an HTTP POST to check the user's response.
     * @param $request a sfRequest object containing the user's submission (just
     * pass from the action).
     * @param $extra_params    an array of extra variables to post to the server.
     * @return reCaptchaResponse object holding the server's response.
     */

    public function checkAnswer(sfWebRequest $request, $extra_params = array())
    {
        if(!empty($this->response))
            return $this->response;

        $privkey = $this->privateKey;
        $remoteip = $request->getHttpHeader ('addr','remote');
        $challenge = $request->getParameter('recaptcha_challenge_field');
        $response = $request->getParameter('recaptcha_response_field');
        $result = new reCaptchaResponse();                
                
        try {
            if(empty($privkey))
            {
                throw new reCaptchaException("Your reCAPTCHA private key is missing.");
            }
            if(empty($remoteip))
            {
                throw new reCaptchaException("reCAPTCHA requires the remote IP to function. It cannot be empty or null.");
            }
            if(empty($challenge) || empty($response))
            {
                
                throw new reCaptchaException("Incorrect CAPTCHA solution.");
            }
            $httpresponse = $this->httpPost(self::RECAPTCHA_VERIFY_SERVER, "/recaptcha/api/verify",
                    array(
                        'privatekey' => $privkey,
                        'remoteip' => $remoteip,
                        'challenge' => $challenge,
                    'response' => $response,
                ) + $extra_params
            );
            $answers = explode("\n", $httpresponse[1]);
            $result->is_valid = (trim($answers[0]) == 'true') ? true : false;
            if(!$result->is_valid)
                $result->error = $answers[1];
        } catch(reCaptchaException $ex) {
            $result->is_valid = false;
            $result->error = $ex->getMessage();
            if(!$request->hasParameter('recaptcha_challenge_field'))
            {
                sfContext::getInstance()->getLogger()->debug('WARNING: no challenge found. Is your form including recaptcha_challenge_field in the submission?');
            }
            if(!$request->hasParameter('recaptcha_response_field'))
            {
                sfContext::getInstance()->getLogger()->debug('WARNING: no response found. Is your form including recaptcha_response_field in the submission?');
            }
            sfContext::getInstance()->getLogger()->debug('sfReCaptchaPlugin: checkAnswer: '.$ex->getMessage());
        }
        $this->response = $result;
        return $this->response;
    }

    /**
     * Helper function to generate an URL for users to sign up for reCAPTCHA. 
     * If your application has a configuration page where users can enter their
     * reCAPTCHA keys, use this method to generate your signup link.
     * @param $domain The domain where the page is hosted.
     * @param $appname The name of your application.
     */
    public function getSignupUrl($domain = null, $app = null)
    {
        return "https://www.google.com/recaptcha/admin/create?" .  $this->qsEncode(array ('domains' => $domain, 'app' => $appname));
    }

    /**
     * Create an URL to allow users to view the e-mail address. This function
     * requires the ability to encrypt, and will return an empty string if it
     * cannot perform the encryption. Any such errors will be logged to your
     * application's log.
     * 
     * @param $email the e-mail address to encrypt.
     * @return a link to reCAPTCHA mailhide, or an empty string if the link
     * cannot be generated.
     */
    public function mailhideUrl($email)
    {
        $pubkey = $this->publicKey;
        $privkey = $this->privateKey;
        
        try {
            if(empty($pubkey) || empty($privkey))
            {
                throw new reCaptchaException("You need a private and public key to use reCAPTCHA Mailhide.");
            }
            $ky = pack('H*', $privkey);
            $cryptmail = $this->aesEncrypt($email, $ky);
    
            return "http://www.google.com/recaptcha/mailhide/d?k=" . $pubkey . "&c=" . $this->mailhideUrlBase64($cryptmail);
        } catch(reCaptchaException $ex) {
            sfContext::getInstance()->getLogger()->debug('sfReCaptchaPlugin: checkAnswer: '.$ex->getMessage());
            return '';
        }
    }
    /**
     * Generate the HTML to render the mailhide links and mangle the e-mail address
     * to prevent scraping. If the link cannot be generated (due to missing
     * encryption module), an error is logged and an error is rendered in place
     * of the link.
     * 
     * @param type $email the e-mail address to mangle.
     * @return HTML code that renders the mailhide link to the user.
     */
    public function getMailhideHTML($email)
    {
        try {
            $emailparts = $this->mailhideEmailParts($email);
            $url = $this->mailhideUrl($email);
    
            return htmlentities($emailparts[0]) . "<a href='" . htmlentities ($url) .
        "' onclick=\"window.open('" . htmlentities ($url) . "', '', 'toolbar=0,scrollbars=0,location=0,statusbar=0,menubar=0,resizable=0,width=500,height=300'); return false;\" title=\"Reveal this e-mail address\">...</a>@" . htmlentities ($emailparts [1]);
        } catch(reCaptchaException $ex) {
            sfContext::getInstance()->getLogger()->debug('sfReCaptchaPlugin: checkAnswer: '.$ex->getMessage());
            return $this->renderError($ex->getMessage());
        }
    }

    /**
     * Encodes the given data into a query string format.
     * @param $data An array of string elements to be encoded.
     * @return the encoded request.
     */

    private function qsEncode($data)
    {
        $result = "";
        foreach($data as $key => $value)
            $result .= $key . '=' . urlencode(stripslashes($value)) . '&';

        // Cut the last '&'
        $result=substr($result,0,strlen($result)-1);

        return $result;
    }

    /**
     * Lengthen (pad) the given data so it fills the minimum block size (16 characters).
     * @param $val the data to be padded, if necessary.
     * @return the padded version of $val.
     */

    private function aesPad($val)
    {
        $block_size = 16;
        $numpad = $block_size - (strlen($val) % $block_size);
        return str_pad($val, strlen($val)+$numpad, chr($numpad));
    }

    /**
     * Perform the encryption. The passed data will be padded as necessary. Throws
     * a reCaptchaException if encryption is not available.
     * 
     * @param type $val the data to be encrypted.
     * @param type $ky The reCAPTCHA private key, packed via pack('H*', $privateKey)
     * @return the encrypted data.
     */
    private function aesEncrypt($val, $ky)
    {
        if($this->canEncrypt)
        {
            $mode = MCRYPT_MODE_CBC;
            $enc = MCRYPT_RIJNDAEL_128;
            $val = $this->aesPad($val);
            return mcrypt_encrypt($enc, $ky, $val, $mode, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0");
        }
        throw new reCaptchaException("reCAPTCHA Mailhide requires the mcrypt PHP module, which appears to be missing.");
    }
    /**
     * Submits an HTTP POST to a reCAPTCHA server.
     * @param $host the domain name of the server.
     * @param $path the path to submit the request to.
     * @param $data a keyed array of strings to be converted into POST data.
     * @param $port (optional) the TCP/IP port to connect to (default is port 80, the standard HTTP port).
     * @return the HTTP response in an indexed array.
     */

    private function httpPost($host, $path, $data, $port=80)
    {
        $postdata = $this->qsEncode($data);

        $http_request  = "POST $path HTTP/1.0\r\n";
        $http_request .= "Host: $host\r\n";
        $http_request .= "Content-Type: application/x-www-form-urlencoded;\r\n";
        $http_request .= "Content-Length: " . strlen($postdata) . "\r\n";
        $http_request .= "User-Agent: reCAPTCHA/PHP\r\n";
        $http_request .= "\r\n";
        $http_request .= $postdata;

        $http_response = '';
        $fs = @fsockopen($host, $port, $errno, $errstr, 10);
        if($fs == false)
        {
            throw new reCaptchaException("httpPost: Could not open socket: ${errstr} (${errno})");
        } else {
            fwrite($fs, $http_request);
            while (!feof($fs))
                        $http_response .= fgets($fs, 1160); // One TCP-IP packet
            fclose($fs);
            $http_response = explode("\r\n\r\n", $http_response, 2);
            return $http_response;
        }
    }

    private function mailhideUrlBase64($x)
    {
        return strtr(base64_encode($x), '+/', '-_');
    }

    /**
     * Gets the parts of the e-mail address to expose to the user.
     * e.g. given 'johndoe@example.com', return ["john", "example.com"].
     * The e-mail address is then displayed as 'john...@example.com'.
     * @param $email an e-mail address to mangle.
     * @return an array containing the displayable fragments.
     */
    private function mailhideEmailParts($email)
    {
        $arr = preg_split("/@/", $email );

        if (strlen ($arr[0]) <= 4)
        {
            $arr[0] = substr ($arr[0], 0, 1);
        } else if (strlen ($arr[0]) <= 6) {
            $arr[0] = substr ($arr[0], 0, 3);
        } else {
            $arr[0] = substr ($arr[0], 0, 4);
        }

        return $arr;
    }
}